package com.clear.honghefinance.biz.modules.system.role.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.clear.honghefinance.biz.modules.system.menu.converter.SysMenuConverter;
import com.clear.honghefinance.biz.modules.system.menu.service.ISysMenuService;
import com.clear.honghefinance.biz.modules.system.resource.service.ISysResourceService;
import com.clear.honghefinance.biz.modules.system.role.converter.SysRoleConverter;
import com.clear.honghefinance.biz.modules.system.menu.domain.entity.SysMenu;
import com.clear.honghefinance.biz.modules.system.resource.domain.entity.SysResource;
import com.clear.honghefinance.biz.modules.system.role.domain.dto.*;
import com.clear.honghefinance.biz.modules.system.role.domain.entity.SysRole;
import com.clear.honghefinance.biz.modules.system.member.domain.vo.AdminDTO;
import com.clear.honghefinance.biz.modules.system.role.domain.vo.GetSysRoleDetailVO;
import com.clear.honghefinance.biz.modules.system.role.domain.vo.ListRoleBindMenuVO;
import com.clear.honghefinance.biz.modules.system.role.domain.vo.ListSysRoleVO;
import com.clear.honghefinance.biz.modules.system.role.mapper.SysRoleMapper;
import com.clear.honghefinance.biz.modules.system.role.service.ISysRoleBindMenuService;
import com.clear.honghefinance.biz.modules.system.role.service.ISysRoleBindResourceService;
import com.clear.honghefinance.biz.modules.system.role.service.ISysRoleService;
import com.clear.honghefinance.common.constant.CommonConstant;
import com.clear.honghefinance.common.dto.entity.PageInfo;
import com.clear.honghefinance.common.dto.entity.PageResult;
import com.clear.honghefinance.common.exception.BizException;
import com.clear.honghefinance.common.service.TokenService;
import com.clear.honghefinance.common.util.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 系统角色 服务实现类
 * </p>
 *
 * @author 程序员clear
 * @since 2024-08-15
 */
@Service
@Slf4j
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements ISysRoleService {

    @Resource
    private TokenService<AdminDTO> tokenService;
    @Resource
    private ISysMenuService sysMenuService;
    @Resource
    private ISysResourceService sysResourceService;
    @Resource
    private ISysRoleBindMenuService sysRoleBindMenuService;
    @Resource
    private ISysRoleBindResourceService sysRoleBindResourceService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 新增角色
     *
     * @param dto req
     * @return
     */
    @Override
    public boolean create(CreateSysRoleDTO dto) {
        if (log.isInfoEnabled()) {
            log.info("新增角色中，入参：{}", JSON.toJSONString(dto));
        }
        SysRole sysRole = new SysRole();
        sysRole.setRoleName(dto.getRoleName());
        sysRole.setDisable(dto.getDisable());
        sysRole.initDefault();
        sysRole.setMemberId(tokenService.getThreadLocalUserId());
        sysRole.setUpdateMemberId(sysRole.getMemberId());
        try {
            return this.save(sysRole);
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 修改角色
     *
     * @param dto req
     * @return
     */
    @Override
    public boolean update(UpdateSysRoleDTO dto) {
        if (log.isInfoEnabled()) {
            log.info("修改角色中，入参：{}", JSON.toJSONString(dto));
        }
        try {
            LambdaUpdateWrapper<SysRole> updateWrapper = Wrappers.<SysRole>lambdaUpdate().eq(SysRole::getId, dto.getId()).eq(SysRole::getDelFlag, false).set(SysRole::getRoleName, dto.getRoleName());
            return this.update(updateWrapper);
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 删除角色
     *
     * @param dto req
     * @return
     */
    @Override
    public boolean delete(DeleteSysRoleDTO dto) {
        if (log.isInfoEnabled()) {
            log.info("删除角色中，入参：{}", JSON.toJSONString(dto));
        }
        // 逻辑删除
        try {
            LambdaUpdateWrapper<SysRole> updateWrapper = Wrappers.<SysRole>lambdaUpdate().eq(SysRole::getId, dto.getId()).set(SysRole::getDelFlag, true).set(SysRole::getUpdateMemberId, tokenService.getThreadLocalUserId()).set(SysRole::getUpdateTime, DateUtil.getSystemTime());
            //todo 后续通过mq发送消息调用删除
            if (this.update(updateWrapper)) {
                // 删除角色绑定的菜单缓存
                deleteSysRoleMenuCache(dto.getId());
                //删除角色绑定的资源缓存
                deleteSysRoleResourceCache(dto.getId());
                return true;
            } else {
                if (log.isInfoEnabled()) {
                    log.info("删除角色失败，入参：{}", JSON.toJSONString(dto));
                }
                throw new BizException("删除角色失败");
            }
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 禁用或启用角色
     *
     * @param dto req
     * @return
     */
    @Override
    public boolean updateDisable(UpdateSysRoleStateDTO dto) {
        if (log.isInfoEnabled()) {
            log.info("{}角色中，入参：{}", dto.getDisable() ? "禁用" : "启用", JSON.toJSONString(dto));
        }
        try {
            LambdaUpdateWrapper<SysRole> updateWrapper = Wrappers.<SysRole>lambdaUpdate()
                    .eq(SysRole::getId, dto.getId())
                    .eq(SysRole::getDisable, !dto.getDisable()) // 状态与传入的不同才会改变
                    // 只改变为删除的角色的禁用状态
                    .eq(SysRole::getDelFlag, false)
                    .set(SysRole::getDisable, dto.getDisable())
                    .set(SysRole::getUpdateMemberId, tokenService.getThreadLocalUserId())
                    .set(SysRole::getUpdateTime, DateUtil.getSystemTime());
            if (this.update(updateWrapper)) {
                if (dto.getDisable()) {  // 禁用
                    // 禁用角色，删除角色绑定的菜单缓存
                    deleteSysRoleMenuCache(dto.getId());
                    // 删除角色绑定的资源缓存
                    deleteSysRoleResourceCache(dto.getId());
                } else {  // 启用
                    // 启用角色，设置角色绑定的菜单缓存
                    setSysRoleMenuCache(dto.getId());
                    // 设置角色绑定的资源缓存
                    setSysRoleResourceCache(dto.getId());
                }
            } else {
                throw new BizException(String.format("%s角色失败", dto.getDisable() ? "禁用" : "启用"));
            }
            return true;
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 查看角色列表
     *
     * @param dto req
     * @return
     */
    @Override
    public PageResult<ListSysRoleVO> list(ListSysRoleDTO dto) {
        if (log.isInfoEnabled()) {
            log.info("查看角色列表中，入参：{}", JSON.toJSONString(dto));
        }
        try {
            IPage<SysRole> page = new Page<>();
            page.setCurrent(dto.getPageNum());
            page.setSize(dto.getPageSize());

            // select * from sys_role like %roleName% and disable = disable
            LambdaUpdateWrapper<SysRole> lambdaUpdateWrapper = Wrappers.<SysRole>lambdaUpdate()
                    .like(StringUtils.isNotBlank(dto.getRoleName()), SysRole::getRoleName, dto.getRoleName())
                    .eq(Objects.nonNull(dto.getDisable()), SysRole::getDisable, dto.getDisable())
                    // 根据更新时间倒序
                    .orderByDesc(SysRole::getUpdateTime);

            this.page(page, lambdaUpdateWrapper);
            List<SysRole> records = page.getRecords();

            PageResult<ListSysRoleVO> pageResult = new PageResult<>();
            if (CollectionUtils.isNotEmpty(records)) {
                List<ListSysRoleVO> list = records.stream().map(item -> {
                    ListSysRoleVO vo = new ListSysRoleVO();
                    vo.setId(item.getId());
                    vo.setRoleName(item.getRoleName());
                    vo.setDisable(item.getDisable());

                    return vo;
                }).collect(Collectors.toList());
                pageResult.setList(list);
                pageResult.setPageNum(dto.getPageNum());
                pageResult.setPageSize(dto.getPageSize());
                pageResult.setTotal((int) page.getTotal());
                pageResult.setPages((int) page.getPages());
            }
            return pageResult;
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 查看角色明细
     *
     * @param id 角色id
     */
    @Override
    public GetSysRoleDetailVO getDetailById(int id) {
        log.info("查看角色明细中，入参：{}", id);
        try {
            LambdaQueryWrapper<SysRole> lambdaQueryWrapper = Wrappers.<SysRole>lambdaQuery().eq(SysRole::getId, id).eq(SysRole::getDelFlag, false);
            SysRole sysRole = this.getOne(lambdaQueryWrapper);
            if (Objects.isNull(sysRole)) {
                throw new BizException("角色不存在");
            }
            return SysRoleConverter.INSTANCE.convertSysRoleEntityToSysRoleDetailVO(sysRole);
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 角色绑定菜单
     *
     * @param dto req
     * @return
     */
    @Override
    public boolean roleBindMenu(RoleBindMenuDTO dto) {
        if (log.isInfoEnabled()) {
            log.info("新增角色绑定菜单中，入参：{}", JSON.toJSONString(dto));
        }
        try {
            if (CollectionUtils.isNotEmpty(dto.getBindMenuIds())) {
                // 1. 校验菜单id是否合法
                List<SysMenu> sysMenus = sysMenuService.listMenuByIds(dto.getBindMenuIds());
                if (dto.getBindMenuIds().size() != sysMenus.size()) {
                    throw new BizException("菜单非法");
                }
            }
            // 2. 角色绑定菜单
            boolean flag = sysRoleBindMenuService.roleBindMenu(dto);
            // 3. 缓存到redis：将该角色对应的所有菜单缓存到redis
            if (flag) {
                setSysRoleMenuCache(dto.getRoleId());
            }
            return true;
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 查看当前登录用户角色绑定的菜单列表（用户绑定的角色可能有很多）
     *
     * @return
     */
    @Override
    public List<ListRoleBindMenuVO> listRoleBindMenu() {
        // 1.获取当前用户角色id列表
        Set<Long> roleIds = tokenService.getThreadLocalUser().getSysRoleIds();
        // 2.获取当前用户角色绑定的菜单列表
        // ==优化从缓存获取==
        return this.listRoleMenuIdByCache(roleIds);

        /*
        角色1：菜单1、菜单2、菜单3、菜单4、菜单5...
            菜单1：子菜单4、子菜单5...
            菜单2：...
            菜单3：...
        角色2：菜单1、菜单2、菜单4...
            ....
        ....
        */
    }

    /**
     * 将某个角色绑定的菜单存入到缓存中
     *
     * @param roleId 角色id
     */
    private void setSysRoleMenuCache(int roleId) {
        // 1.查询所有的菜单
        List<SysMenu> sysMenus = sysMenuService.listMenu();
        // 2.查询角色绑定的菜单id列表
        List<Integer> menuIds = sysRoleBindMenuService.listBindMenuIdByRoleId(roleId);
        // 3.将菜单列表和角色绑定的菜单id列表组装成菜单树
        List<ListRoleBindMenuVO> listRoleBindMenuVOS = listRoleBindMenu(roleId, menuIds, sysMenus);
        updateSysRoleMenuCache(roleId, listRoleBindMenuVOS);
    }

    /**
     * 更新角色菜单路由缓存
     *
     * @param roleId              角色id
     * @param listRoleBindMenuVOS 菜单路由
     */
    private void updateSysRoleMenuCache(int roleId, List<ListRoleBindMenuVO> listRoleBindMenuVOS) {
        String cacheKey = CommonConstant.ROLE_MENU_PERMISSIONS;
        HashOperations<String, String, List<ListRoleBindMenuVO>> hashOps = redisTemplate.opsForHash();
        hashOps.put(cacheKey, String.valueOf(roleId), listRoleBindMenuVOS);
    }

    /**
     * 删除角色资源路由缓存
     *
     * @param roleId 角色id
     */
    private void deleteSysRoleMenuCache(int roleId) {
        String cacheKey = CommonConstant.ROLE_MENU_PERMISSIONS;
        HashOperations<String, String, List<ListRoleBindMenuVO>> hashOps = redisTemplate.opsForHash();
        hashOps.delete(cacheKey, String.valueOf(roleId));
    }

    /**
     * 从缓存中获取角色绑定的菜单列表
     *
     * @param roleIds 角色id列表
     * @return
     */
    private List<ListRoleBindMenuVO> listRoleMenuIdByCache(Set<Long> roleIds) {
        HashOperations<String, String, List<ListRoleBindMenuVO>> hashOps = redisTemplate.opsForHash();
        String key = CommonConstant.ROLE_MENU_PERMISSIONS;
        Set<String> hashKey = roleIds.stream().map(String::valueOf).collect(Collectors.toSet());
        List<List<ListRoleBindMenuVO>> roleMenuIds = hashOps.multiGet(key, hashKey);

        // 对结果进行处理，将 List<List<Integer>> 转为 List<Integer>
        return roleMenuIds.stream()
                .filter(p -> !CollectionUtils.isEmpty(p))
                .flatMap(List::stream).collect(Collectors.toList());
    }

    /**
     * 查询角色绑定的菜单列表
     *
     * @return
     */
    private List<ListRoleBindMenuVO> listRoleBindMenu(int roleId, List<Integer> menuIds, List<SysMenu> sysMenus) {
        Set<Integer> parentIds = sysMenus.stream()
                .filter(p -> (CommonConstant.ROLE_ADMIN == roleId || menuIds.contains(p.getId())) && p.getPid() > 0)
                .map(SysMenu::getPid)
                .collect(Collectors.toSet());
        // 查询所有子节点
        List<SysMenu> parentMenus = sysMenus.stream()
                .filter(p -> parentIds.contains(p.getId()))
                .collect(Collectors.toList());
        List<ListRoleBindMenuVO> parentMenuDataItemVos = SysMenuConverter.INSTANCE.convertSysMenuEntityListToListRoleBindMenuVO(parentMenus);
        for (ListRoleBindMenuVO parentMenuDataItemVo : parentMenuDataItemVos) {
            if (parentMenuDataItemVo.getLayout()) {
                parentMenuDataItemVo.setLayout(null);
            }
            List<SysMenu> childMenus = sysMenus.stream().filter(p -> p.getPid().equals(parentMenuDataItemVo.getId())
                            && (CommonConstant.ROLE_ADMIN == roleId || menuIds.contains(p.getId())))
                    .collect(Collectors.toList());
            parentMenuDataItemVo.setRoutes(SysMenuConverter.INSTANCE.convertSysMenuEntityListToListRoleBindMenuVO(childMenus).stream()
                    .peek(p -> {
                        if (p.getLayout()) {
                            p.setLayout(null);
                        }
                    }).collect(Collectors.toList()));
        }
        return parentMenuDataItemVos;
    }


    /**
     * 角色绑定资源
     *
     * @param dto req
     * @return
     */
    @Override
    public boolean roleBindResource(RoleBindResourceDTO dto) {
        if (log.isInfoEnabled()) {
            log.info("角色绑定资源中，入参：{}", JSON.toJSONString(dto));
        }
        try {
            if (CollectionUtils.isNotEmpty(dto.getBindResourceIds())) {
                // 1.校验资源id是否合法
                List<SysResource> sysResources = sysResourceService.listResourceByIds(dto.getBindResourceIds());
                if (dto.getBindResourceIds().size() != sysResources.size()) {
                    throw new BizException("资源非法");
                }
            }
            // 2. 角色绑定资源
            boolean flag = sysRoleBindResourceService.roleBindResource(dto);
            // 3. 缓存到redis：将该角色对应的所有资源缓存到redis
            if (flag) {
                setSysRoleResourceCache(dto.getRoleId());
            }
            return true;
        } catch (Exception e) {
            throw new BizException(e.getMessage(), e);
        }
    }

    /**
     * 将所有角色绑定的菜单列表刷写到缓存（通过定时任务触发）
     */
    @Override
    public void setSysRoleBindMenuCache() {
        PageInfo pageInfo = new PageInfo();
        pageInfo.setPageNum(1);
        pageInfo.setPageSize(100);

        // 获取所有菜单
        List<SysMenu> sysMenus = sysMenuService.listMenu();
        // 获取角色列表
        List<SysRole> sysRoles;
        while (CollectionUtils.isNotEmpty(sysRoles = this.listByPage(pageInfo))) {
            for (SysRole sysRole : sysRoles) {
                // 清理缓存
                if (sysRole.getDisable() || sysRole.getDelFlag()) {
                    deleteSysRoleMenuCache(sysRole.getId());
                    continue;
                }
                // 查看角色绑定的菜单id列表
                List<Integer> menuIds = sysRoleBindMenuService.listBindMenuIdByRoleId(sysRole.getId());
                // 查询角色绑定的菜单列表
                List<ListRoleBindMenuVO> listRoleBindMenuVOS = listRoleBindMenu(sysRole.getId(), menuIds, sysMenus);

                // 更新缓存
                updateSysRoleMenuCache(sysRole.getId(), listRoleBindMenuVOS);
            }
            pageInfo.setPageNum(pageInfo.getPageNum() + 1);
        }
    }

    /**
     * 将所有角色绑定的资源列表刷写到缓存（通过定时任务触发）
     */
    @Override
    public void setSysRoleBindResourceCache() {
        PageInfo pageInfo = new PageInfo();
        pageInfo.setPageNum(1);
        pageInfo.setPageSize(100);

        // 获取角色列表
        List<SysRole> sysRoles;
        while (CollectionUtils.isNotEmpty(sysRoles = this.listByPage(pageInfo))) {
            for (SysRole sysRole : sysRoles) {
                // 清理缓存
                if (sysRole.getDisable() || sysRole.getDelFlag()) {
                    deleteSysRoleResourceCache(sysRole.getId());
                    continue;
                }
                // 查看角色绑定的资源id列表
                List<Integer> resourceIds = sysRoleBindResourceService.listBindResourceIdByRoleId(sysRole.getId());
                // 查询角色绑定的资源列表
                List<SysResource> sysResources = sysResourceService.listResourceByIds(resourceIds);

                // 更新缓存
                updateSysRoleResourceCache(sysRole.getId(),
                        sysResources.stream().map(SysResource::getPath).collect(Collectors.toSet()));
            }
            pageInfo.setPageNum(pageInfo.getPageNum() + 1);
        }
    }

    /**
     * 查看角色列表 分页查询
     *
     * @param dto
     * @return
     */
    private List<SysRole> listByPage(PageInfo dto) {
        IPage<SysRole> page = new Page<>(dto.getPageNum(), dto.getPageSize());
        LambdaQueryWrapper<SysRole> lambdaQueryWrapper = Wrappers.<SysRole>lambdaQuery()
                // 注释掉，是有意为之，为了清理缓存
//                .eq(SysRole::getDisable, false)
//                .eq(SysRole::getDelFlag, false)
                .orderByDesc(SysRole::getUpdateTime);
        this.page(page, lambdaQueryWrapper);

        List<SysRole> records = page.getRecords();
        return records;
    }

    /**
     * 将某个角色绑定的资源设置到缓存中
     *
     * @param roleId 角色id
     */
    private void setSysRoleResourceCache(int roleId) {
        // 查询绑定的资源id列表
        List<Integer> resourceIds = sysRoleBindResourceService.listBindResourceIdByRoleId(roleId);
//        List<SysResource> sysResources = sysResourceService.listByIds(resourceIds);
        // 解决 where id in () 括号中为空导致的sql错误
        List<SysResource> sysResources = sysResourceService.listResourceByIds(resourceIds);
        if (CollectionUtils.isEmpty(sysResources)) {
            updateSysRoleResourceCache(roleId, new HashSet<>());
        } else {
            updateSysRoleResourceCache(roleId,
                    sysResources.stream().map(SysResource::getPath).collect(Collectors.toSet()));
        }
    }

    /**
     * 更新角色资源路由缓存
     *
     * @param roleId       角色id
     * @param resourcePath 资源路由
     */
    private void updateSysRoleResourceCache(int roleId, Set<String> resourcePath) {
        String cacheKey = CommonConstant.ROLE_RESOURCE_PERMISSIONS;
        HashOperations<String, String, Set<String>> hashOps = redisTemplate.opsForHash();
        hashOps.put(cacheKey, String.valueOf(roleId), resourcePath);
    }

    /**
     * 删除角色资源路由缓存
     *
     * @param roleId 角色id
     */
    private void deleteSysRoleResourceCache(int roleId) {
        String cacheKey = CommonConstant.ROLE_RESOURCE_PERMISSIONS;
        HashOperations<String, String, Set<String>> hashOps = redisTemplate.opsForHash();
        hashOps.delete(cacheKey, String.valueOf(roleId));
    }
}
